基本的にはgithubに すべてが書かれている. リファレンスはここから 確認すること. 探すと色々と関数があるのはわかるけど,いまいちjoinについて 記法がわからない. いわゆるleft_joinはあるのだが,inner, outerなどがわかればより 有り難いのだが・・・・. overlapjoinnon-equijoinなどがある. by = .EACHIはどのような意味なのだろうか? each i を示す特別な記号であるのはわかったが.

target_libs <- c(
    "data.table", 
    "fs", 
    "purrr", 
    "ggplot2"
)
for (lib in target_libs) {
    if (!require(lib, character.only = TRUE)) {
        try({install.packages(lib); library(lib, character.only = TRUE)})
    }
}

# constant
DATA_DIR <- "D"

Introduction to data.table

ここ のチュートリアルをやる.

Data

データの読み込みはfread関数を使う. freadは、http/httpsをインプットファイルとして指定することも可能である.

input <- path(DATA_DIR, "flights14.csv")

flights <- fread(input)
flights[1:5]
print(class(flights))
## [1] "data.table" "data.frame"

Introduction

Basices

data.tabledata.frameを強化したものを与えるRのパッケージである. インスタンスの作成はfreadによるものあるし,下記のようにdata.table関数で 作成することが可能である.

DT = data.table(
    ID = c("b", "b", "b", "a", "a", "c"), 
    a = 1:6, 
    b = 7:12, 
    c = 13:18
)
DT
print(class(DT))
## [1] "data.table" "data.frame"

ほかにも,setDTas.data.tableから作成することができる. 前者はdata.framelistの構造を対象としたもので,後者はそれ以外を対象としたものである.

# 既存のオブジェクトを変換する際には, copyしておく必要がある. 
iris_dt <- copy(iris)
print(list(address(iris), address(iris_dt)))
## [[1]]
## [1] "00000000261cb3b0"
## 
## [[2]]
## [1] "0000000020738e68"
# オブジェクトは受けとることもできるし実際には
# これだけでdata.tableになっている
setDT(iris_dt)

iris_dt
print(class(iris_dt))
## [1] "data.table" "data.frame"
as.data.table(iris)
d <- list(
    a = letters, 
    b = LETTERS, 
    c = seq_along(letters)
)
setDT(d)
d

ところで,data.frameに対して何が強化されているのか. data.frameに対して,行や列の選択ができるようになっていること 以外にも様々なことが[ ... ]のように角括弧を通じて行える.

generalシンタックスは次である. i, j, byがそれぞれSQLと対応しているとうイメージになる.

DT[i, j, by]

  R:                 i                 j        by
SQL:  where | order by   select | update  group by
  • subset/reorder rows using i
  • calculate j
  • grouped by by

さっそく実践してみる.

# j,byが必要ない場合にはカンマを省略することができる
# カンマをつけていても問題はない
ans <- flights[origin == "JFK" & month == 6L]
head(ans)

ソートをしてみる. この例ではoriginを昇順により並び変えた上で, destを降順で並び変える. このとき使われているorderはベースRのものである.

ans <- flights[order(origin, -dest)]
head(ans)

さて,列を選択してみる. 単純にはベクトルで返される.

ans <- flights[, arr_delay]
head(ans)
## [1]  13  13   9 -26   1   0

data.table, あるいは複数列を選択する場合には, listを使うこと.

ans <- flights[, list(arr_delay)]
head(ans)

data.tableではlistの代わりとして.()を使うことができる. .()listのエイリアスである.

ans <- flights[, .(arr_delay, dep_delay)]
head(ans)
identical(
    flights[, .(arr_delay, dep_delay)], 
    flights[, list(arr_delay, dep_delay)]
    
)
## [1] TRUE

.()listのエイリアスであることがわかっていれば, 次のように列名の変更が有効であることがわかる.

ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)]
head(ans)

さてjを使い計算をしてみる.

ans <- flights[, sum((arr_delay + dep_delay) < 0)]
ans
## [1] 141814

なにが起こったのだろうか.

実はjというのは列を選択するだけでなく, 列を使った表現式を扱うことが可能なのである.

6月のJFK空港を基点としたすべての到着と発着における送れについて,平均値を 計算してみる.

ans <- flights[origin == "JFK" & month == 6L, 
               .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]

ans

カウントについてもお手の物である.

ans <- flights[origin == "JFK" & month == 6L, .(n = length(dest))]
ans

スペシャルシンボルの.Nを使うこともできる.

# そのまま使うときに出てくるこの値はなんの値なのだろうか?
print(.N)
## NULL
ans <- flights[origin == "JFK" & month == 6L, .N]
ans
## [1] 8422

上述した操作は次の操作と同値である. しかし,次の操作は,1度全体のコピーを作成して,そこに関数を適用していることから, 上述した操作と比べて非効率である.

nrow(flights[origin == "JFK" & month == 6L])

文字列としてのカラム名を使い参照することも可能である.

ans <- flights[, c("arr_delay", "dep_delay")]
head(ans)

変数としてカラムの文字列を有している時には次の2つの使い方がある.

select_cols = c("arr_delay", "dep_delay")
flights[, ..select_cols][1:10]
flights[, select_cols, with = FALSE][1:10]

他にもカラム選択のやり方として,非選択を選択することも可能である.

ans <- flights[, !select_cols, with = FALSE]
ans[1:10]
ans <- flights[, -..select_cols]
ans[1:10]

ここから,ここまで,というやり方でも列を選択することが可能である.

flights[, year:day][1:10]
flights[, !(year:day)][1:10]
flights[, -(year:day)][1:10]

Aggregations

byがどのようにグルーピングとして機能するのかを見ていく. ここまでをちゃんと読んでいれば次の操作で起こっていることは明らかである. もちろん変数は1つなので,.()を使う必要はない.

ans <- flights[, .(.N), by = .(origin)]
ans[1:10]
ans <- flights[carrier == "AA", .N, by = origin]
ans[1:10]

配列部分で複数の変数をしているできるのは, by引数においても同様.

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
ans[1:10]

byに文字列ベクトルを使うことも可能である.

ans <- flights[carrier == "AA", .N, by = c("origin", "dest")]
ans[1:10]

aggregationは複数の変数に対しても行える.

ans <- flights[
        carrier == "AA", 
        .(ad_mean = mean(arr_delay), dd_mean = mean(dep_delay)), 
        by = .(origin, dest, month)
    ]
head(ans)

ところで,上記の結果において,origi, dest, monthの順にオーダーをつけたい場合には どのようにすれば良いのか. 次の操作のようにkeybyを使う. 昇順でしか使えないのかな?

ans <- flights[
    carrier == "AA", 
    .(ad_mean = mean(arr_delay), dd_mean = mean(dep_delay)), 
    keyby = .(origin, dest, month)
]
head(ans)

DTは処理を,つまり[ ]をつなげていくことができる. これにより,結果をソートなども中間変数を用意せずに行うことが可能である.

ans <- flights[
    carrier == "AA", 
    .(.N), 
    by = .(origin, dest)
][
    order(origin, -dest)
]
head(ans)

i, jと同様にbyは表現式をハンドリングできる. これを見ると列名を直接参照しなくてもよい,つまり, いわゆるmutateのシンタックスが使われていると考えればよい.

ans <- flights[, .(N = .N), by = .(dep_delay > 0, arr_delay > 0)]
ans[1:10]

ところで,平均値をすべての列に計算したいとする. もちろんすべての変数に対してmean(column)を記述するのは実践的ではない.

data.tableパッケージでは,スペシャルシンボルの.SDを提供している. .SDは「subset of data」であり, byを使い定義した現在のカレントグループのデータそのものを参照する.

DT[1:10]
# byで分割された状態で, 彼とデータをプリントしているので
# 次のような結果となる
DT[, print(.SD), by = ID][1:10]
##    a b  c
## 1: 1 7 13
## 2: 2 8 14
## 3: 3 9 15
##    a  b  c
## 1: 4 10 16
## 2: 5 11 17
##    a  b  c
## 1: 6 12 18

上記のように, .SDがデータのサブセットを参照しているので, これに対してlapplyを使うことができる. つまり, サブデータセットのカラムごとに演算が可能となる. data.tableがdata.tableでもあることの利点がわかった気がする.

DT[, lapply(Filter(is.numeric, .SD), mean, na.rm = TRUE), by = ID][1:10]

ポイントとしてはlapplyが返すのはlistのため, .()でラップする必要はないということ.

.SDcols引数では,カラムネームかカラムのインデックスを受け付けることができる. .SDcolsで指定したカラムだけがSDに含まれてることを補償することができる. カラムの指定は非選択のシンタックスや,colA:colBといったことも可能である.

flights[
    carrier == "AA", 
    lapply(.SD, mean), 
    by = .(origin, dest, month), 
    .SDcols = c("arr_delay", "dep_delay")
][1:10]

最初の2行だけ抽出するということも可能である.

ans <- flights[, head(.SD, 2), by = month]
head(ans)

カラムa, bをIDグループごとに結合するには?

DT[, .(val = c(a, b)), by = ID][1:10]

a, b列のすべての値を結合した値を持つがリストとして返すには? サマラズしているということだね!

DT[, .(val = list(c(a, b)), by = ID)][1:10]

Reference semanticcs

add/update/deleteに関する辞書,とi, byとの連携について示す.

Data

flights <- fread(path(DATA_DIR, "flights14.csv"))
flights[1:10]

Introduction

reference semanticeに関するブリーフな議論と,:=オペレータを 使うことができる2つの形を見るなどをしていく.

Reference semantics

DF <- 
  data.frame(
    ID = c("b","b","b","a","a","c"),
    a = 1:6, 
    b = 7:12, 
    c = 13:18
  )

DF

data.frameで作成したデータに対して,次の2つの操作は どちらもすべてのデータがコピーされる.

print(address(DF))
## [1] "000000001ad76828"
print(address(DF$a))
## [1] "000000001a04b2a8"
print(address(DF$c))
## [1] "000000001a04b468"
# ディープコピーが生じる その1
DF$c <- 18:13
print(address(DF))
## [1] "000000001a7773c0"
print(address(DF$a))
## [1] "000000001a04b2a8"
print(address(DF$c))
## [1] "0000000018649550"
# ディープコピーが生じる その2
DF$c[DF$ID == "b"] <- 15:13
print(address(DF))
## [1] "000000001a7f9a00"
print(address(DF$a))
## [1] "000000001a04b2a8"
print(address(DF$c))
## [1] "000000001a7f9af0"

というが,バージョン4では事情が異なるよう. 上の結果を下記にまとめる.

  • 全体を参照するアドレスは,その1,その2の操作で変わる
  • 操作していないカラムのアドレスは,どれも同じ
  • 操作したカラムはアドレスが変わる.

これはどうなのだろうか?結局のところポインターが変わるのがつまり, データをディープコピーしているわけではなさそう.

ところで,:=オペレータは上記の2つの操作でどちらもインプレイスで処理するため ディープコピーを作成しない. らしいが,下記の結果をみると一応, 列を変更するとその列のアドレスは変更されているのだが・・・?

DT <- setDT(DF)
DT

アドレスが変わる操作と変わらない操作があるようです.

print(address(DT$a))
## [1] "000000001a385828"
DT[, a := 10]
print(address(DT$a))
## [1] "000000001a385828"
DT[, a := a * 10]
print(address(DT$a))
## [1] "0000000019cf0eb0"

複数行の処理が行える.

DT[, c("a2", "b2") := lapply(.SD, function (x) x * x), .SDcols = c("a", "b")]
DT
library(purrr)
## 
## Attaching package: 'purrr'
## The following object is masked from 'package:data.table':
## 
##     transpose
nrm_v <- DT[, c("b", "b2")] %>% lapply(function(x) sd(x) / mean(x))
DT[, ":="(b = nrm_v[[1]])]
## Warning in `[.data.table`(DT, , `:=`(b = nrm_v[[1]])): 0.196929 (type
## 'double') at RHS position 1 truncated (precision lost) when assigning to type
## 'integer' (column 3 named 'b')
DT

ということでえ本編に戻る.

flights[, ":="(speed = distance / (air_time / 60 ), 
               delay = arr_delay + dep_delay)]

head(flights)
# get all hours in flights
flights[, sort(unique(hour))]
##  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

24時を0時に直す

flights[hour == 24L, hour := 0L][]
flights[, sort(unique(hour))]
##  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

複数の列を掃除に操作するやり方.

in_cols = c("dep_delay", "arr_delay")
ot_cols = c("max_dep_delay", "max_arr_delay")
# with = FALSEにするか,c(ot_cols)にしないとそれが列名として解釈されることに注意な
# ..ot_colsも効果がなかった
flights[, ot_cols := lapply(.SD, min), by = month, .SDcols = in_cols, with = FALSE][]
## Warning in `[.data.table`(flights, , `:=`(ot_cols, lapply(.SD, min)), by =
## month, : with=FALSE together with := was deprecated in v1.9.4 released Oct 2014.
## Please wrap the LHS of := with parentheses; e.g., DT[,(myVar):=sum(b),by=a] to
## assign to column name(s) held in variable myVar. See ?':=' for other examples.
## As warned in 2014, this is now a warning.

:=は参照系なので副作用がある.

foo <- function(DT) {
  DT[, speed := distance / (air_time/60)]
  DT[, .(max_speed = max(speed)), by = month]
}
ans = foo(flights)
head(flights[, "speed"]) # speedがある!
head(ans)

参照系で処理しないようにするにはcopy関数を使うこと.

flights[, speed := NULL]
foo <- function(DT) {
  DT <- copy(DT)                              ## deep copy
  DT[, speed := distance / (air_time/60)]     ## doesn't affect 'flights'
  DT[, .(max_speed = max(speed)), by = month]
}
ans <- foo(flights)
names(flights)
##  [1] "year"          "month"         "day"           "dep_delay"    
##  [5] "arr_delay"     "carrier"       "origin"        "dest"         
##  [9] "air_time"      "distance"      "hour"          "delay"        
## [13] "max_dep_delay" "max_arr_delay"
head(ans)

コピーにより副作用なしで演算が出来ていることがわかる. ということだが,いまのバージョンでは上記をシャローコピーで 実現ができるらしい.

DT <- data.table(x = 1L, y = 2L)
DT_n <- names(DT)


# add new column 
DT[, z := 3L]

# DT_n にも追加されている
DT_n
## [1] "x" "y" "z"
DT_n <- copy(names(DT))
DT[, w := 4L]

# copyのときにはアップデートされていない
DT_n
## [1] "x" "y" "z"

ところでggplot2は使えるのか? つかえました.

library(ggplot2)
ans %>% 
  ggplot() + 
  geom_line(aes(x = month, y = max_speed)) + 
  theme_dark()

Keys and fas binary search based subset

Data

flights <- fread(path(DATA_DIR, "flights14.csv"))
head(flights)

Introduction

このチュートリアルの目的は次です.

  • keyとはなにか,高速バイナリーサーチに使うにはどうすれば良いのか
  • keyを使った結合
  • multとnomatchの使い方
  • keyを設定するメリットとはなにか

Keys

What is a key?

ここまでは, iにおいてデータのサブセットを抽出するとき, 論理式,行番号,orderを使ったものを紹介してきた. ここでは,keyを使った高速な処理を紹介する.

とりあえず,データフレームをつくってみる.

set.seed(1L)
DF <- data.frame(
  ID1 = sample(letters[1:2], 10, TRUE), 
  ID2 = sample(1:3, 10, TRUE), 
  val = sample(10), 
  stringsAsFactors = FALSE, 
  row.names = sample(LETTERS[1:10])
)
DF
rownames(DF)
##  [1] "I" "D" "G" "A" "B" "E" "C" "J" "F" "H"

data.frameにおいてもrownameを使いデータのサブセットを作成する ことが可能である.

DF["C", ]

上記の例からrownameというのは,行番号へのインデックスの 役割があることがわかる. ただし,ユニークである必要がある.

ここで,data.tableに変換してみる.

DT <- as.data.table(DF)
DT
rownames(DT)
##  [1] "1"  "2"  "3"  "4"  "5"  "6"  "7"  "8"  "9"  "10"

data.tableに変換することでrow nameはリセットされていることがわかる. data.tableは決してrow nameは使わない. data.tableはdata.frameを継承しているため, row name属性を有しているが,使わない.この理由は後でみられる. もしrow nameを保持したいときにはkeep.rownames = TRUEという 引数を使うこと.

data.tableではkeysを設定,利用する. keysは超高速なrownamesと考えれば良い. keysの特徴は次である.

  • keyは単一,あるいは,複数のカラムに設定できる
    • 設定できるデータ型は, integer, numeric, cahracter, factor, integer64など
    • list, complexには設定できない
  • ユニークであることは強制されない
    • keyでソートするとき,同じkeyが連続して現れるようになる
  • keyの設定は2ステップである
    • 参照形式で与えられれたカラムに基づき常に昇順で行を物理的にreorderする
    • 使ったカラムはkeyカラムとしてフラグが立ち,sorted属性になる

一般的にはkeyは1つのカラムに対して設定する.

set, get and use keys on data.table

setkey(flights, origin)
head(flights)
  • setkeyvを使うと,文字列ベクトルを使いkeyを設定することが可能
  • 上記のようにinplaceで設定が行える
  • 設定したカラムを使いreorderされている
  • data.table関数でインスタンスを作成するときにkey引数で直接指定可能

keyが設定されたdata.tableでは, .()iで使い, サブセットを作成することが可能となる.

flights[.("JFK")][1:5]
key(flights)
## [1] "origin"

Keys and multiple columns

setkey(flights, origin, dest)
head(flights)
setkeyv(flights, c("origin", "dest"))

key(flights)
## [1] "origin" "dest"

2つのkeyを設定したのでそれぞれを指定したサブセットが作成可能.

flights[.("JFK", "MIA")][1:5]

上記の処理はまず“JFK”をoriginから探し, その後に“MIA”をdestから探索している.

2つ目のkeyだけを使いサブセットを作るには1つ工夫が必要となる.

flights[.(unique(origin), "MIA")][1:5]

Combining keys with j and by

Select in j

key(flights)
## [1] "origin" "dest"
flights[.("LGA", "TPA"), .(arr_delay)]
flights[.("LGA", "TPA"), .(arr_delay)][order(-arr_delay)][1:5]

もちろんjにおける演算と組み合わせることができる.

flights[.("LGA", "TPA"), max(arr_delay)]
## [1] 486
# get all "hours" in flights
flights[, sort(unique(hour))]
##  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

すでにやったexampleであるが,keyによる参照においても次のように サブセットに対する処理が可能.

setkey(flights, hour)
key(flights)
## [1] "hour"
flights[.(24), hour := 0L]
key(flights)
## NULL

上記の処理ではkeyに使ったカラムhourを修正した. これによりhourの昇順の整列がくずれた. よって,keyはNULLを設定するという方法を使い, 自動で取り除かれている.

flights[, sort(unique(hour))]
##  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

集計処理についても同様にkeyを駆使して処理することが可能である.

setkey(flights, origin, dest)
key(flights)
## [1] "origin" "dest"
ans <- flights["JFK", .(max_dep_delay = max(dep_delay)), keyby = month]
head(ans)
# 自動でkeyが設定されている
key(ans)
## [1] "month"

Additional arguments mult and nomatch

mutl argument

任意のクエリーにおいて,すべての行を返すのか, 最初あるいは最後の行を返すのかを指定することができる.

flights[.("JFK", "MIA"), mult = "first"]
# JFK-XNAはデータがないのでNAが返されている
flights[.(c("JFK", "LGA", "EWR"), "XNA"), mult = "last"]

the nomatch argument

マッチしないデータを除くことができる.

flights[.(c("JFK", "LGA", "EWR"), "XNA"), mult = "last", nomatch = NULL]

binary serch vs vectors scan

ところで,keyのメリットはなんなのだろうか.

# このようなサブセットの作成はベクトルスキャンでも可能である
identical(
  flights[.("JFK", "MIA")], 
  flights[origin == "JFK" & dest == "MIA"]
)
## [1] TRUE

記法が短いという意味合いもあるが, 端的にいって高速であることがアドバンテージである.

set.seed(2L)
N = 2e7L
DT = data.table(x = sample(letters, N, TRUE),
                y = sample(1000L, N, TRUE),
                val = runif(N))
print(object.size(DT), units = "Mb")
## 381.5 Mb
t1 <- system.time(ans1 <- DT[x == "g" & y == 877L])
t1
##    user  system elapsed 
##    0.75    0.08    0.29
head(ans1)
dim(ans1)
## [1] 762   3
setkeyv(DT, c("x", "y"))
key(DT)
## [1] "x" "y"
t2 <- system.time(ans2 <- DT[.("g", 877L)])
t2
##    user  system elapsed 
##       0       0       0
head(ans2)
dim(ans2)
## [1] 762   3
identical(ans1$val, ans2$val)
## [1] TRUE

このように高速になるのは, データがソート済みになっていることで バイナリーサーチが使えるためである. 通常のベクトルスキャンでは毎回すべての要素との比較が行われており, なんどもサブセットを作成する情況においては非効率である.

Secondary indices and auto indexing

flights <- fread(path(DATA_DIR, "flights14.csv"))
head(flights)
dim(flights)
## [1] 253316     11

Introduction

  • 第2インデックスとその有用性について
  • 高速なサブセット化をonを使っておこなう
    • テンポラリーがだ再利用が可能
  • 自動インデックス,自動第2インデックス

Secondary indices

What are secondary indices

第二インデックスとは,keyと似たものであるが次の点が異なる

  • 物理的に行の順番を変更するわけでない
    • index属性で管理されている
  • 1つ以上の第2インデックスを与えることができる

Set and get secondary indices

setindex(flights, origin)
head(flights)
names(attributes(flights))
## [1] "names"             "row.names"         "class"            
## [4] ".internal.selfref" "index"
  • setindex / setindexvで設定する
  • 物理的に行の番号が変更されたわけでhない
  • index属性が追加されている
  • setindex(flights, NULL)によりインデクスは削除
indices(flights)
## [1] "origin"
setindex(flights, origin, dest)
# 2つ目が少しかわっているが複数のインデックスが作成できることがわかる
indices(flights)
## [1] "origin"       "origin__dest"

ところでなぜセカンダリーインデックスが必要であるのか?

1つにはデータを物理的に並び変えるのはコストが大きい場合があり, 常に理想的な処理であるとは言えない. たとえば参照するカラムとkeyが異なる場合, そのたびにリオーダーが発生する. このような場合にセカンダリーが有効になる.

セカンダリーは,複数のインデックスを,属性として保持することが できるのでリオーダーの時間を節約することができる. on引数は自動でセカンダリーを作成する.

Fast subsetting using on arguent and secondary indices

flights["JFK", on = "origin"][1:5]

上記の処理でオンザフライでインデックスが作成されている. 処理速度はバイナリーサーチを用いた場合と同様である. ただし,インデックスは自動では保存されていない. これについては今後変更される予定である.

もし既にセカンダリーをsetindexで作成しているときには, onはセカンダリーを再利用する. このことはverbose= TRUEによりみることができる.

setindex(flights, origin)
flights["JFK", on = "origin", verbose = TRUE][1:5]
## i.V1 has same type (character) as x.origin. No coercion needed.
## on= matches existing index, using index
## Starting bmerge ...
## forder.c received 1 rows and 1 columns
## bmerge done in 0.000s elapsed (0.000s cpu) 
## Constructing irows for '!byjoin || nqbyjoin' ... 0.000s elapsed (0.000s cpu)

カラムが複数あるときにはベクトルで指定すればよい.

flights[.("JFK", "LAX"), on = c("origin", "dest")][1:5]

jとの組み合わせは既にみてきたkeyがある場合の 組み合わせと同じである. 属性にindexが与えられることだけに注意する.

flights[.("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")][1:5]

チャイニングも同じである.

flights[
    .("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")
  ][
  order(-arr_delay)][
    1:5
  ]

サマライズも同じ.

flights[
  .("LGA", "TPA"), 
  max(arr_delay), 
  on = c("origin", "dest")
]
## [1] 486

サブアサインも同じ.

flights[, sort(unique(hour))]
##  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
flights[.(24L), hour := 0L, on = "hour"]
flights[, sort(unique(hour))]
##  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

集計も同じ. iにインデックス記法を使わない場合には, onは必要ない. keybyはkeyを与えているのではなくて,順序化したグループ化の変数で, 自動でkeyが与えられる. ちなみにoriginカラムはなくなるのでインデックスは付いていない...

ans <- flights[
  "JFK", 
  max(dep_delay), 
  keyby = month, 
  on = "origin"
]
ans[1:5]
key(ans)
## [1] "month"
indices(ans)
## NULL

multも正しく動く.

flights[c("BOS", "DAY"), on = "dest", mult = "first"]

nomatchも動く.

flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", 
        on = c("origin", "dest"), 
        nomatch = NULL]

AUto indexing

set.seed(1L)
dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
print(object.size(dt), units = "Mb")
## 114.4 Mb
names(attributes(dt))
## [1] "names"             "row.names"         "class"            
## [4] ".internal.selfref"
(t1 <- system.time(ans <- dt[x == 989L]))
##    user  system elapsed 
##    1.05    0.09    0.36
head(ans)
names(attributes(dt))
## [1] "names"             "row.names"         "class"            
## [4] ".internal.selfref" "index"
# 自動で作成されている
indices(dt)
## [1] "x"

1度目の参照時間はインデックスを作成する時間と, サブセットを抽出する時間の和になっている. 自動インデックスはoptions(datatable.auto.index = FALSE)により 停止することも出来るが,実際にはベクタースキャンよりも 2回目以降は下に示すようにより高速に行える. 現在のところ自動インデックスは==%in%の 場合に設定される.

(t2 <- system.time(dt[x == 989L]))
##    user  system elapsed 
##       0       0       0
system.time(dt[x %in% 1989:2012])
##    user  system elapsed 
##    0.01    0.00    0.02
setindex(dt, NULL)
# <= のようなオペレータではインデックスは作成されない
dt[x <= 2000][1:5]
indices(dt)
## NULL

Efficient reshapng using data.tables

Data

input <- path(DATA_DIR, "flights14.csv")
flights <- fread(input)
flights[1:10]

Introduction

meltdcastはdata.tableのための縦横変換関数であり, 10GBを超えるin memoryのデータを想定sちえ実装されていr.

Default functionality

melting

s1 <- "family_id age_mother dob_child1 dob_child2 dob_child3
1         30 1998-11-26 2000-01-29         NA
2         27 1996-06-22         NA         NA
3         26 2002-07-11 2004-04-05 2007-09-02
4         32 2004-10-10 2009-08-27 2012-07-21
5         29 2000-12-05 2005-02-28         NA"
DT <- fread(s1)
DT

meltを使い再構築する.

DT.m1 <- melt(
  DT, 
  
  # IDとして使う変数(age_motherはいつ年齢?)
  id.vars = c("family_id", "age_mother"), 
  # 観測値の値として使う変数
  measure.vars = c("dob_child1", "dob_child2", "dob_child3")
)
DT.m1

meltと同じに名前を与える.

DT.m1 <- melt(
  DT, 
  # IDとして使う変数(age_motherはいつ年齢?)
  id.vars = c("family_id", "age_mother"), 
  # 観測値の値として使う変数
  measure.vars = c("dob_child1", "dob_child2", "dob_child3"), 
  # 観測値の変数名
  variable.name = "child", 
  # 観測値の値の変数名
  value.name = "dob"
)
DT.m1

dcasting

dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")

横持ちに変換する際には,データを集計することが可能である.

dcast(
  DT.m1, 
  family_id ~ ., 
  fun.agg = function(x) sum(!is.na(x)), 
  value.var = "dob"
)

これは通常は次のように処理が行える.

DT.m1[!is.na(dob), .(.N), keyby = "family_id"]

Limitations in current melt/dcast approaches

簡単に記述できない場合を考えて見る.

s2 <- "family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
1         30 1998-11-26 2000-01-29         NA             1             2            NA
2         27 1996-06-22         NA         NA             2            NA            NA
3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
5         29 2000-12-05 2005-02-28         NA             2             1            NA"
DT <- fread(s2)
DT

dobとgenderを観測値の変数としてまとめることを考える. いまある機能を使うと次のように, 1度全体を融かして,変数部分だけ分離させるという方法で記述ができる.

DT.m1 <- melt(DT, id = c("family_id", "age_mother"))
DT.m1[, c("variable", "child") := tstrsplit(variable, "_", fixed = TRUE)]
DT.m1[1:5]
DT.c1 <- dcast(DT.m1, family_id + age_mother + child ~ variable, 
               value.var = "value")

DT.c1[1:10]

しかし,この方法には次の問題点がある.

  • dob, genderの変数の型が異なる場合の対処
  • 中間変数variableの意図が不明確

stats::reshapeこの問題を簡単に解決してくれる. (自分で試してみるべし!とのこと)

Enhanced functionality

Enhanced melt

カラムのリストをmeasure.varsに渡すことで解決する.

colA = paste("dob_child", 1:3, sep = "")
colB = paste("gender_child", 1:3, sep = "")
DT.m2 = melt(DT, 
             measure = list(colA, colB), 
             value.name = c("dob", "gender"))
DT.m2

すべてを指定することが難しい場合にはpatternsを使うこと.

DT.m2 <- 
  melt(
    DT, 
    # measureでもいいみたいだが,上との対応のため,measure.varsを使う
    # 本当は用途が異なる可能性が多分にある・・・
    measure.vars = patterns ("^dob", "^gender"), 
    value.name = c("dob", "gender")
  )
DT.m2

Enhanced dcast

DT.c2 <- 
  dcast(
    DT.m2, 
    family_id + age_mother ~ variable, 
    value.var = c("dob", "gender")
  )
DT.c2

Cheet Sheet

cheet sheetを一通りなめる.

dt <- data.table(
  a = 1:5, 
  b = 6:10, 
  c = c("a", "a", "b", "b", "b")
)

Manipulate columns with j

Extract

dt[, c(2)]
dt[, !2]
dt[, .(b, c)]
dt[, c("a", "b")]

文字だけの場合にはどちらでも良い.

dt[, "b"]

文字列を値とした変数を使う場合はやり方が二通りある.

cln <- c("a", "b")
dt[, ..cln]
dt[, cln, with = FALSE]

summarise

代表値を求める関数を列の位置で使うことができる.

dt[, sum(a)]
## [1] 15
dt[, .(a = sum(a))]

compute columns

inplaceで変更する.

dt[, d := LETTERS[1:5]]
dt
dt[, a := a * 10]
dt
dt[a  > mean(a), b := a + b]
dt

複数列に対して演算するときには少し複雑.

dt[a > mean(a), ":="(c = toupper(c), d = tolower(d))]
dt

DELETE COLUMN

dt[, d := NULL]
dt

convert column type

dt[, b := as.complex(b)]
dt

Group according to by

byは,.SDに適用した結果をbind_rowsするイメージ

dt[, .SD, by = .(c)]

プリントすると,.SDがそれぞれdata.tableであることが わかる.

dt[, print(.SD), by = c]
##     a    b
## 1: 10 6+0i
## 2: 20 7+0i
##     a    b
## 1: 30 8+0i
##     a     b
## 1: 40 49+0i
## 2: 50 60+0i
# あまり意味のない処理だけど
dt[, .SD[,"b"], by = c]

集約処理も簡単に行える.

dt[, .(c = sum(b)), by = a]
dt
# .SDはdata.tableなので1行目のデータをデータフレームが参照
dt[, .SD[1], by = a]
dt[, .SD[.N], by = a]
dt

Chaining

[ ]演算子を重ねていくことでチェインできる. byは適用されないもよう.

dt[, .SD, by = c][, sum(a)]
## [1] 150

Function for data.tables

REORDER

setorder(dt, -b, a)
dt

UNIQUE ROWS

# byを指定しないとすべての列が使われる
unique(dt, by = c("a", "b"))
dt

RENAME COLUMNS

setnames(dt, c("a"), c("a_new"))
dt

ユニークなカラム数を見極める.

uniqueN(dt)
## [1] 5
dt[, .(n = uniqueN(.SD)), by = c]

SET KEYS

dt[.(value), ]を使ったカラム指定における 高速な繰り返し参照を実現する. また,カラムを指定しないマージdt_a[dt_b]の高速化にも繋がる.

setkey(dt, a, b)
dt
key(dt)
## [1] "a" "b"

Combine data.table

JOIN

dt1 <- data.table(
  a = 1:3,
  b = c("a", "b", "c")
)
dt2 <- data.table(
  x = 3:1,
  y = c("b", "c", "a")
)
# 新しいインスタンスが作成されている
# dt1, dt2には副作用はない
dt1[dt2, on = .(b = y)]
dt_a <- copy(dt1)[, c := c(7, 5, 6)]
dt_b <- copy(dt2)[, z := c(4, 5, 8)]

dt_a[dt_b, on = .(b = y, c > z)]

ROLLING JOIN

dt_a <-
  data.table(
    a    = c(1, 2, 3, 1, 2), 
    id   = c("A", "A", "A", "B", "B"), 
    date = seq(as.Date("2020/1/1"), by = "3 days", length.out = 5)
  )

dt_a
dt_b <- 
  data.table(
    b = c(1, 1), 
    id = c("A", "B"), 
    date = seq(as.Date("2020/1/2"), by = "2 days", length.out = 2)
  )
dt_b

通常のジョインだとマッチはしない.

dt_a[dt_b, on = .(id = id, date = date)]

ローリングだと最も近い直前の値でマッチすることが出来る. マッチしないとNAになっていることがわかる.

dt_a[dt_b, on = .(id = id, date = date), roll = TRUE]

BIND

rbind(dt1, dt1)
cbind(dt1, dt1)

Reshape a data.tabel

RESHAPE TO WIDE FORMART

dt <- 
  data.table(
    id = c("A", "A", "B", "B"), 
    y  = c("x", "z", "x", "z"), 
    a  = c(1, 2, 1, 2), 
    b  = c(3, 4, 3, 4)
  )
dt
dt_wide <- 
  dcast(
    dt, 
    id ~ y, 
    value.var = c("a", "b")
  )
dt_wide

RESHAPE TO LONG FORMART

dt_long <- 
  melt(
    dt_wide, 
    id.vars = c("id"), 
    measure.vars = patterns("^a"), 
    variable.name = "y", 
    value.name = c("a")
  )
dt_long

複数行に対してもできるが,variableが因子になるのに注意.

  melt(
    dt_wide, 
    id.vars = c("id"), 
    measure.vars = patterns("^a", "^b"), 
    variable.name = "y", 
    value.name = c("a", "b")
  )

Apply function to cols

dt[, lapply(.SD, mean), .SDcols = c("a", "b")]

Sequantial rows

ROW IDS

dt[, c:=1:.N, by = b]
dt

LAG & LEAD

dt[, c:=shift(a, 1), by = b][order(a, b)]
dt[, c:=shift(a, 1, type = "lead"), by= b][order(a, b)]
dt

Examples

ここの Examplesから, ここまでであまり理解していなかった記述方法について試す.

DF = data.frame(x=rep(c("b","a","c"),each=3), y=c(1,3,6), v=1:9)
DT = data.table(x=rep(c("b","a","c"),each=3), y=c(1,3,6), v=1:9)

DF
DT
# 環境に存在する data.tableを一覧
tables()
##        NAME    NROW NCOL MB
##  1:     ans     101    2  0
##  2:    ans1     762    3  0
##  3:    ans2     762    3  0
##  4:       d      26    3  0
##  5:      dt       4    4  0
##  6:      DT       9    3  0
##  7:   DT.c1      15    5  0
##  8:   DT.c2       5    8  0
##  9:   DT.m1      30    5  0
## 10:   DT.m2      15    5  0
## 11:    dt_a       5    3  0
## 12:    dt_b       2    3  0
## 13: dt_long       4    3  0
## 14: dt_wide       2    5  0
## 15:     dt1       3    2  0
## 16:     dt2       3    2  0
## 17: flights 253,316   11 14
## 18: iris_dt     150    5  0
##                                                          COLS
##  1:                                                       x,y
##  2:                                                   x,y,val
##  3:                                                   x,y,val
##  4:                                                     a,b,c
##  5:                                                  id,y,a,b
##  6:                                                     x,y,v
##  7:                     family_id,age_mother,child,dob,gender
##  8:       family_id,age_mother,dob_1,dob_2,dob_3,gender_1,...
##  9:                 family_id,age_mother,variable,value,child
## 10:                  family_id,age_mother,variable,dob,gender
## 11:                                                 a,id,date
## 12:                                                 b,id,date
## 13:                                                    id,y,a
## 14:                                        id,a_x,a_z,b_x,b_z
## 15:                                                       a,b
## 16:                                                       x,y
## 17:            year,month,day,dep_delay,arr_delay,carrier,...
## 18: Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species
##                            KEY
##  1:                           
##  2:                           
##  3:                        x,y
##  4:                           
##  5:                           
##  6:                           
##  7: family_id,age_mother,child
##  8:       family_id,age_mother
##  9:                           
## 10:                           
## 11:                           
## 12:                           
## 13:                           
## 14:                         id
## 15:                           
## 16:                           
## 17:                           
## 18:                           
## Total: 14MB

fast ad hoc row subsets (subsets as joins)

DT[["v"]]
## [1] 1 2 3 4 5 6 7 8 9
DT[, sum(v), by = x]
DT[, sum(v), keyby = x]
DT["a", on = "x"] # same as DT[x == "a"]
DT["a", on = .(x)]
DT[.("a"), on = "x"]
indices(DT)
## NULL
# これは内部でバイナリーサーチが行われてる
DT[x == "a"]
DT[.("b", 3L), on = c("x", "y")]
# データがない部分も参照できる
DT[.("b", 1:2), on = c("x", "y")]
# ローリグ参照
# 欠損するときには,それよりも前の値を参照
DT[.("b", 1:2), on = c("x", "y"), roll = Inf]
# ローリング参照
# 欠損するときには,それよりも後の値を参照
DT[.("b", 1:2), on = c("x", "y"), roll = - Inf]
DT[.("b", 1:2), on = c("x", "y"), roll = 0]
DT[.("b", 1:2), on = c("x", "y"), roll = 1]
DT[x != "a", sum(v), keyby = x]
DT[!"a", .(s = sum(v)), by = .EACHI, on = "x"]

joins as subsets

X = data.table(x=c("c","b"), v=8:7, foo=c(4,2))
X
# right join
DT[X, on = "x"]
# left join
# 向きが直感と逆な気もする...
# イメージとしてはDTのxカラムを,Xから参照するということだろうか..?
X[DT, on = "x"]
# inner join 

DT[X, on = "x", nomatch = NULL]
# anti join
DT[!X, on = "x"]
DT[X, on = c(y = "v")]
# 論理ベクトルを返す文字列で指定することもできる
DT[X, on = "y==v"]
# つまりは, on は変数名でも良いし,ベクトルでもよい
# ということはやはり, on はmutateのシンタックスという理解をしておけばおよい.
DT[X, on = .(y <= foo)]
DT[X, on=c("y<=foo")][order(x)]
DT[X, on=.(y>=foo)] 
DT[X, on = .(x, y <= foo)]
# 上記の結果を名前を設定しながら, 
DT[X, .(x, y, x.y, v), on = .(x, y >= foo)]
DT[X, on = "x", mult = "first"]

.EACHIの便利なところかな? よくわからないけど,on と同じ変数でbyされるということな.

DT[X, .SD, on = c("x")]
DT[X, .SD, by = .EACHI, on = c("x")]
# xでbyされている
DT[X, sum(v), by = .EACHI, on = c("x")]
# 演算としては,Xの変数も使える.
DT[X, sum(v) * foo, by = .EACHI, on = "x"]
# i.cはXのvである. 
# このためsum(v)と次元があっている
DT[X, sum(v) * i.v, by= .EACHI, on = "x"]

setting keys

kDT = copy(DT)                        # (deep) copy DT to kDT to work with it.
setkey(kDT,x)                         # set a 1-column key. No quotes, for convenience.
setkeyv(kDT,"x")                      # same (v in setkeyv stands for vector)
v="x"
setkeyv(kDT,v)                        # same
# key(kDT)<-"x"                       # copies whole table, please use set* functions instead
haskey(kDT)                           # TRUE
## [1] TRUE
#> [1] TRUE
key(kDT)           
## [1] "x"
# データ行がリオーダーされている
kDT
# keyが設定されていればバイナリーサーチが使える
kDT["a"]
indices(kDT)
## NULL
kDT["a", on = "x"]
indices(kDT)
## NULL
 # get sum(v) for each i != "a"
kDT[!"a", sum(v), by= .EACHI]
# 配列にしておけばkeyを設定していなくても動く
kDT[.("a")]

more on special symbols

DT[ , .SD, .SDcols = patterns('^[xv]')]
# .Iはrow number
DT[, .I]  
## [1] 1 2 3 4 5 6 7 8 9
DT[, .I[1], by=x]  
# .GRPはグループナンバー
DT[, .GRP, by = x]
DT[, grp := .GRP, by = x]
DT
DT[, .BY, by= .(x, y)]
# .BYはグープごとのシングルトン
DT[, dput(.BY), by = .(x, y)]
## list(x = "b", y = 1)
## list(x = "b", y = 3)
## list(x = "b", y = 6)
## list(x = "a", y = 1)
## list(x = "a", y = 3)
## list(x = "a", y = 6)
## list(x = "c", y = 1)
## list(x = "c", y = 3)
## list(x = "c", y = 6)

これはなにをしているのかがわかっていない・・

X[, DT[.BY, y, on = "x"], by= x]
DT[, {
  # write each group to a different file
  fwrite(.SD, file.path(tempdir(), paste0('x=', .BY$x, " y = ", .BY$y, '.csv')))
}, by=.(x, y)]
#> Empty data.table (0 rows and 1 cols): x
dir(tempdir())
##  [1] "file3afc11562517" "file3afc152d5dfe" "file3afc18744d99" "file3afc1cfd2dee"
##  [5] "file3afc1e1b6dae" "file3afc1f197455" "file3afc1f525c2"  "file3afc1f5367b7"
##  [9] "file3afc208e40fe" "file3afc21011718" "file3afc22d734df" "file3afc241f1620"
## [13] "file3afc252060d2" "file3afc2914785f" "file3afc304734e1" "file3afc3389ef4" 
## [17] "file3afc350f1cf2" "file3afc36581d89" "file3afc3b7c418f" "file3afc3bc10de" 
## [21] "file3afc3bc6353"  "file3afc3c565afa" "file3afc3f12e68"  "file3afc42fbb02" 
## [25] "file3afc442b6074" "file3afc45aa30ac" "file3afc46055ae3" "file3afc4a5d2259"
## [29] "file3afc4d1e71cd" "file3afc4d33727"  "file3afc4fa160b4" "file3afc54b4463b"
## [33] "file3afc5a202bd6" "file3afc5ae17a72" "file3afc67054d33" "file3afc676e59be"
## [37] "file3afc68d366b0" "file3afc6ada4c9f" "file3afc6cf799d"  "file3afc6ec15cb" 
## [41] "file3afc70f41049" "file3afc73dfcbe"  "file3afc751b7313" "file3afc774732ef"
## [45] "file3afc79974656" "file3afc7b2c4e37" "file3afc7be15d7f" "file3afc7bec6af9"
## [49] "file3afc7e281d63" "file3afc7f804bb2" "file3afc8ab27af"  "file3afc8ae7543" 
## [53] "file3afca7c7591"  "file3afca955126"  "file3afcab5d02"   "x=a y = 1.csv"   
## [57] "x=a y = 3.csv"    "x=a y = 6.csv"    "x=b y = 1.csv"    "x=b y = 3.csv"   
## [61] "x=b y = 6.csv"    "x=c y = 1.csv"    "x=c y = 3.csv"    "x=c y = 6.csv"

advanced usage

DT = data.table(
  x=rep(c("b","a","c"),each=3), 
  v=c(1,1,1,2,2,1,1,2,2), 
  y=c(1,3,6), 
  a=1:9, b=9:1)

DT[, sum(v), by=.(y%%2)] 
DT[, sum(v), by=.(bool = y%%2)]
DT[, list(MySum=sum(v),
          MyMin=min(v),
          MyMax=max(v)),
    by=.(x, y%%2)]     
DT[, .(a = .(a), b = .(b)), by=x]
DT[, .(seq = min(a):max(b)), by=x]

一時変数が欲しい場合には,jで波括弧を使えば良い.

DT[, {tmp <- mean(y);
      .(a = a-tmp, b = b-tmp)
      }, by=x] 
                                 # expression. TO REMEMBER: every element of
                                      # the list becomes a column in result.
pdf("new.pdf")
DT[, plot(a,b), by=x]                 # can also plot in 'j'
#> Empty data.table (0 rows and 1 cols): x
dev.off()
## png 
##   2
#> pdf 
#>   2 
# file.remove("new.pdf")
DT
DT[, rleid := rleid(DT$v)]
# 上記をみると
# rleidはデータの切り替わり順!!!
DT[, c(.(y=max(y)), lapply(.SD, min)), by=rleid(v), .SDcols=v:b]
?rleid
## starting httpd help server ... done
DT = data.table(grp=rep(c("A", "B", "C", "A", "B"), c(2,2,3,1,2)), value=1:10)
DT
rleid(DT$grp) # get run-length ids
##  [1] 1 1 2 2 3 3 3 4 5 5
rleid(DT$grp, prefix="grp") # prefix with 'grp'
##  [1] "grp1" "grp1" "grp2" "grp2" "grp3" "grp3" "grp3" "grp4" "grp5" "grp5"
# get sum of value over run-length groups
DT[, sum(value), by=.(grp, rleid(grp))]
DT[, sum(value), by=.(grp, rleid(grp, prefix="grp"))]

My Note

公式のチュートリアルに出てこない関数たちの調査結果.

  • rowid
  • fsetdiff

functions

rowid

引数が切り替わるごとに1から番号振りを始める

DT = data.table(x=c(20,10,10,30,30,20), y=c("a", "a", "a", "b", "b", "b"), z=1:6)[]
DT
rowid(DT$x) # 1,1,2,1,2,2
## [1] 1 1 2 1 2 2
DT[,  z := rowid(DT$y)]
DT
?rowid
rowidv(DT, cols="x") # same as above
## [1] 1 1 2 1 2 2
rowid(DT$x, prefix="group") # prefixed with 'group'
## [1] "group1" "group1" "group2" "group1" "group2" "group2"
rowid(DT$x, DT$y) # 1,1,2,1,2,1
## [1] 1 1 2 1 2 1
rowidv(DT, cols=c("x","y")) # same as above
## [1] 1 1 2 1 2 1
DT[, .(N=seq_len(.N)), by=.(x,y)]$N # same as above
## [1] 1 1 2 1 2 1
# convenient usage with dcast
dcast(DT, x ~ rowid(x, prefix="group"), value.var="z")

fsetdiff

集合演算が行えるようです.

x  = data.table(c(1,2,2,2,3,4,4))
x2 = data.table(c(1,2,3,4)) # same set of rows as x
y  = data.table(c(2,3,4,4,4,5))

all=FALSEという引数がある. この場合には,重複した行は削除されれる. all=TRUEのとき,は個別にみるイメージである. 逆を言えば, all=FALSEは本当に集合として処理を行うイメージである.

fintersect(x, y)            # intersect
# あくまで交差集合なので,xよりも数が多いyの4などにおいては,
# yの数が残されないことに注意する
fintersect(x, y, all=TRUE)  # intersect all
fsetdiff(x, y)              # except
fsetdiff(x, y, all=TRUE)    # except all
funion(x, y)                # union
funion(x, y, all=TRUE)      # union all
fsetequal(x, x2, all=FALSE) 
## [1] TRUE
fsetequal(x, x2)            # setequal all
## [1] FALSE

foverlaps

ぱっと見だけど区間の話をしているようにみえる. xのstart/endの間において,yのstart/endの間がラップしているのかどうかを判定している. あんまり使いどころがわかっていないが, バイナリーサーチで行われるらしいので必要になったら高速に処理が可能である.

x = data.table(start=c(5,31,22,16), end=c(8,50,25,18), val2 = 7:10)
y = data.table(start=c(10, 20, 30), end=c(15, 35, 45), val1 = 1:3)
setkey(y, start, end)
foverlaps(x, y, type="any", which=TRUE)

これはオーバーラップのなかでも,包含を対象としている.

foverlaps(x, y, type = "within")

uniqueN

a <- sample(letters[1:5], 10, replace = TRUE)
uniqueN(a)
## [1] 5

first

first(1:10)
## [1] 1

frollmean

frollmean(1:10, n = 3, fill = Inf, align = "center")
##  [1] Inf   2   3   4   5   6   7   8   9 Inf

transpose

DT = data.table(x=1:5, y=6:10)
transpose(DT)
## [[1]]
## [[1]]$x
## [1] 1
## 
## [[1]]$y
## [1] 6
## 
## 
## [[2]]
## [[2]]$x
## [1] 2
## 
## [[2]]$y
## [1] 7
## 
## 
## [[3]]
## [[3]]$x
## [1] 3
## 
## [[3]]$y
## [1] 8
## 
## 
## [[4]]
## [[4]]$x
## [1] 4
## 
## [[4]]$y
## [1] 9
## 
## 
## [[5]]
## [[5]]$x
## [1] 5
## 
## [[5]]$y
## [1] 10

truelength

通常のlengthだと,カラム数を返すのだけど, 実際にはアロケートされている列の数が異なるので, アロケートされている分までを出力する.

DT = data.table(a=1:3,b=4:6)
length(DT)                 # 2 column pointer slots used
## [1] 2
truelength(DT)   
## [1] 1026
setalloccol(DT, 2048)
length(DT)                 # 2 used
## [1] 2
truelength(DT)    
## [1] 2050

tstrsplit

traspose(strsplit())と同じ. つまり,は分割した文字列を列ごとに保持するようになる.

# names引数があるのでそれを使うことも良い
a <- data.table(
  x = c("x_1", "x_2", "x")
)[, c("x", "n") := tstrsplit(x, "_", fill = "0")]
a[]

rbindlist

リストに保持された複数のデータテーブルをひとつにまとめたものを 作成する.

DT1 = data.table(A=1:3,B=letters[1:3])
DT2 = data.table(A=4:5,B=letters[4:5])
l = list(DT1,DT2)
rbindlist(l)

カラム名が異なっていても,引数で調整することが可能.

DT1 = data.table(A=1:3,B=letters[1:3])
DT2 = data.table(B=letters[4:5],C=factor(1:2))
l = list(DT1,DT2)
rbindlist(l, use.names=TRUE, fill=TRUE)

set

行と列を指定して,値を設定する関数. 行ごとに値を設定する処理の時に有効にみえる. ただし,基本的に行単位で値を繰り返し設定することは 稀であるので,あまり気にしなくても良い気がする.

m = matrix(1, nrow = 2e6L, ncol = 100L)
DF = as.data.frame(m)
DT = as.data.table(m)
bench::mark(
  system.time(for (i in 1:1000) DF[i, 1] = i), 
  system.time(for (i in 1:1000) DT[i, V1 := i]), 
  system.time(for (i in 1:1000) set(DT, i, 1L, i)), 
  check = FALSE
)
## Warning: Some expressions had a GC in every iteration; so filtering is disabled.
## # A tibble: 3 x 6
##   expression                                            min  median `itr/sec`
##   <bch:expr>                                        <bch:t> <bch:t>     <dbl>
## 1 system.time(for (i in 1:1000) DF[i, 1] = i)          4.7s    4.7s     0.213
## 2 system.time(for (i in 1:1000) DT[i, `:=`(V1, i)])   317ms 393.6ms     2.54 
## 3 system.time(for (i in 1:1000) set(DT, i, 1L, i))   45.9ms  46.9ms    21.2  
## # ... with 2 more variables: mem_alloc <bch:byt>, `gc/sec` <dbl>

nafill

x = 1:10
x[c(1:2, 5:6, 9:10)] = NA
nafill(x, "locf")
##  [1] NA NA  3  4  4  4  7  8  8  8
dt = data.table(v1=x, v2=shift(x)/2, v3=shift(x, -1L)/2)
nafill(dt, "nocb")
## [[1]]
##  [1]  3  3  3  4  7  7  7  8 NA NA
## 
## [[2]]
##  [1] 1.5 1.5 1.5 1.5 2.0 3.5 3.5 3.5 4.0  NA
## 
## [[3]]
##  [1] 1.5 1.5 2.0 3.5 3.5 3.5 4.0  NA  NA  NA
setnafill(dt, "locf", cols=c("v2","v3"))
dt

between

1:10 %between% c(0, 5)
##  [1]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE
1:10 %inrange% c(0, 5)
##  [1]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE

cube/rollup

あんましわかっていないけど, これらの関数を使うとaggregateが明示的に行えるようになる. これを使えば,・・・?ポスグレなどとの対応がわかりやすくなる.

n = 24L
set.seed(25)
DT <- data.table(
    color  = sample(c("green","yellow","red"), n, TRUE),
    year   = as.Date(sample(paste0(2011:2015,"-01-01"), n, TRUE)),
    status = as.factor(sample(c("removed","active","inactive","archived"), n, TRUE)),
    amount = sample(1:5, n, TRUE),
    value  = sample(c(3, 3.5, 2.5, 2), n, TRUE)
)
DT[1:10]
# rollup
rollup(DT, j = sum(value), by = c("color","year","status")) # default id=FALSE
rollup(DT, j = sum(value), by = c("color","year","status"), id=TRUE)
rollup(DT, j = lapply(.SD, sum), by = c("color","year","status"), id=TRUE, .SDcols="value")
cube(DT, j = sum(value), by = c("color","year","status"), id=TRUE)
cube(DT, j = lapply(.SD, sum), by = c("color","year","status"), id=TRUE, .SDcols="value")
dt = data.table(
  x = 1:10, 
  y = rep(c("a", "b"), each = 5)
)

cube(dt, .(n = .N), by = "y", id = TRUE)
groupingsets(
  DT, 
  j = c(list(count=.N), lapply(.SD, sum)), 
  by = c("color","year","status"),
  # setsはidをまとめるもので,今回の場合にはyear, statusがニコイチになる
  sets = list("color", c("year","status"), character()), 
  id=TRUE)

chmatch

高速な文字列マッチング. らしいけど,この例ではそこまで差がないもよう.

N = 1e6
# N is set small here (1e5) to reduce runtime because every day CRAN runs and checks
# all documentation examples in addition to the package's test suite.
# The comments here apply when N has been changed to 1e8 and were run on 2018-05-13
# with R 3.5.0 and data.table 1.11.2.

u = as.character(as.hexmode(1:10000))
y = sample(u,N,replace=TRUE)
x = sample(u)
                                           #  With N=1e8 ...
system.time(a <- match(x,y))               #  4.6s
##    user  system elapsed 
##    0.03    0.00    0.03
system.time(b <- chmatch(x,y))             #  1.8s
##    user  system elapsed 
##    0.02    0.00    0.02
identical(a,b)
## [1] TRUE
system.time(a <- x %in% y)               #  4.5s
##    user  system elapsed 
##    0.03    0.00    0.03
system.time(b <- x %chin% y)             #  1.7s
##    user  system elapsed 
##    0.01    0.00    0.01
identical(a,b)
## [1] TRUE
# Different example with more unique strings ...
u = as.character(as.hexmode(1:(N/10)))
y = sample(u,N,replace=TRUE)
x = sample(u,N,replace=TRUE)
system.time(a <- match(x,y))               # 46s
##    user  system elapsed 
##    0.11    0.00    0.11
system.time(b <- chmatch(x,y))             # 16s
##    user  system elapsed 
##    0.04    0.00    0.05
identical(a,b)
## [1] TRUE